﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using KewlStuff.Extensions;
using MovieModel.Common;
using MovieModel.Model;
using MovieModel.Model.Common;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace MovieModel.Json
{
    public class MovieSerializer
    {
        public void SaveMovies(TextWriter writer, IEnumerable<Movie> movies)
        {
            Contract.Requires(movies != null);
            Contract.Requires(writer != null);
            SaveCommon(writer, "movies", movies, MovieProperties);
        }

        public void SavePeople(TextWriter writer, IEnumerable<Person> people)
        {
            Contract.Requires(people != null);
            Contract.Requires(writer != null);
            SaveCommon(writer, "people", people);
        }

        public void SaveTags(TextWriter writer, IEnumerable<Tag> tags)
        {
            Contract.Requires(tags != null);
            Contract.Requires(writer != null);
            SaveCommon(writer, "tags", tags);
        }

        public void SaveCompanies(TextWriter writer, IEnumerable<Company> companies)
        {
            Contract.Requires(companies != null);
            Contract.Requires(writer != null);
            SaveCommon(writer, "companies", companies);
        }

        private static IEnumerable<JProperty> CommonProperties<T>(T i) where T : MovieItem<T>, new()
        {
            IEnumerable<JProperty> props = new[]
            {
                new JProperty("Id", i.Id),
                new JProperty("Name", i.Name),
                new JProperty("SearchMatches", ToJArray(i.SearchMatches))
            };
            if (i is IImageItem)
            {
                props = props.Union(new[]
                                        {
                                            new JProperty("ThumbnailLocation", ((IImageItem)i).ThumbnailLocation),
                                            new JProperty("ImageLocation", ((IImageItem)i).ImageLocation)
                                        });
            }
            return props;
        }

        public void SaveCommon<T>(TextWriter writer, string name, IEnumerable<T> items, Func<T, IEnumerable<JProperty>> itemProperties = null) where T : MovieItem<T>, new()
        {
            Contract.Requires(writer != null);
            Contract.Requires(items != null);
            if (itemProperties == null) itemProperties = i => new JProperty[0];
            var list = from i in items
                       select new JObject(CommonProperties(i).Union(itemProperties(i)));
            var root = new JObject(new JProperty(name, ToJArray(list)));
            var serializer = new JsonSerializer();
            serializer.Serialize(writer, root);
            writer.Close();
        }

        private static JArray ToJArray<T>(IEnumerable<T> list)
        {
            if (list == null) return new JArray();
            return new JArray(list);
        }

        private static JArray ToJArray<T, TR>(IEnumerable<T> list, Func<T, TR> mutator)
        {
            Contract.Requires(list == null || mutator != null);
            if (list == null) return new JArray();
            return new JArray(list.Select(mutator));
        }

        public IDictionary<int, Movie> ParseMovies(TextReader reader, Manager manager)
        {
            return ParseItems<Movie>(reader, "movies", (movie, json) => AddMovieFields(movie, json, manager));
        }

        public IDictionary<int, Person> ParsePeople(TextReader reader)
        {
            return ParseItems<Person>(reader, "people", null);
        }

        public IDictionary<int, Company> ParseCompanies(TextReader reader)
        {
            return ParseItems<Company>(reader, "companies", null);
        }

        public IDictionary<int, Tag> ParseTags(TextReader reader)
        {
            return ParseItems<Tag>(reader, "tags", null);
        }

        private static IEnumerable<JProperty> MovieProperties(Movie movie)
        {
            return
                new[]
                    {
                        new JProperty("MovieFile", movie.MovieFile.FullName), 
                        new JProperty("Runtime", movie.Runtime),
                        new JProperty("Year", movie.Year),
                        new JProperty("Description", movie.Description), 
                        new JProperty("Companies", ToJArray(movie.Companies, c => c.Id)),
                        new JProperty("Tags", ToJArray(movie.Tags, t => t.Id)),
                        new JProperty("MovieParts",
                                      ToJArray(movie.People,
                                               p => new JObject(
                                                        new JProperty("Person", p.Person.Id),
                                                        new JProperty("Role", Enum.GetName(typeof (Role), p.Role)))))
                    };
        }

        private static void AddMovieFields(Movie movie, JToken json, Manager manager)
        {
            var companies = json["Companies"] as JArray;
            if (companies != null) movie.Companies = (from c in companies select manager.Companies[c.Value<int>()]).ToArray();

            var parts = json["MovieParts"];
            if (parts != null)
                movie.People = from p in parts
                               select new MoviePart(manager.People[p["Person"].Value<int>()],
                                                    (Role)Enum.Parse(typeof(Role), p["Role"].Value<string>()),
                                                    movie);

            movie.Runtime = json["Runtime"].Value<int?>();
            movie.Year = json["Year"].Value<int?>();
            movie.Description = json["Description"].SafeValue<string>();
            movie.MovieFile = new FileInfo(json["MovieFile"].Value<string>());
            var tags = json["Tags"] as JArray;
            if (tags != null) movie.Tags = (from t in tags select manager.Tags[t.Value<int>()]).ToArray();
        }

        private static IDictionary<int, T> ParseItems<T>(TextReader textReader, string name, Action<T, JToken> loadFields) where T : MovieItem<T>, new()
        {
            using (textReader)
            {
                JObject root;
                try
                {
                    root = JToken.ReadFrom(new JsonTextReader(textReader)) as JObject;
                }
                catch (JsonReaderException e)
                {
                    throw new UserFriendlyException("Malformed tags: invalid json: " + e.Message);
                }
                if (root == null) throw new UserFriendlyException("Malformed " + name + ": No root object");
                var objs = root[name] as JArray;
                if (objs == null) throw new UserFriendlyException("Malformed " + name + ": no \"" + name + "\" array");
                
                var ret = new Dictionary<int, T>();
                foreach (var tagTok in objs)
                {
                    var obj = tagTok as JObject;
                    if (obj == null) throw new UserFriendlyException("Malformed " + name + ": non-object found in \"" + name + "\" array");

                    try
                    {
                        if (obj["Id"] == null) throw new UserFriendlyException("Malformed " + name + ": Item missing Id");
                        var id = obj["Id"].Value<int>();
                        var t = new T {Id = id};
                        LoadCommonFields(t, obj, name);
                        if (loadFields != null) loadFields(t, obj);
                        ret[id] = t;
                    }
                    catch (Exception e)
                    {
                        throw new UserFriendlyException("Malformed " + name + ": Object not parseable (" + e.Message + "): " + obj, e);
                    }
                }
                return ret;
            }
        }

        private static void LoadCommonFields<T>(MovieItem<T> item, JObject jsonObject, string name) where T : MovieItem<T>, new()
        {
            item.Name = jsonObject["Name"].SafeValue<string>();
            if (item.Name == null) throw new UserFriendlyException("Malformed " + name + " [" + item.Id + "] : Missing name");
            var sm = jsonObject["SearchMatches"];
            if (sm != null) item.SearchMatches = sm.Select(t => t.Value<string>());
            var imageItem = item as ImageMovieItem<T>;
            if (imageItem == null) return;
            imageItem.ImageLocation = jsonObject["ImageLocation"].SafeValue<string>();
            imageItem.ThumbnailLocation = jsonObject["ThumbnailLocation"].SafeValue<string>();
        }
    }
}
